/*****************************************************************************
*
*  PROJECT:     Multi Theft Auto v1.2
*  LICENSE:     See LICENSE in the top level directory
*  FILE:        FileSystem/src/CFileSystem.cpp
*  PURPOSE:     File management
*  DEVELOPERS:  S2 Games <http://savage.s2games.com> (historical entry)
*               Martin Turski <quiret@gmx.de>
*
*  Multi Theft Auto is available from http://www.multitheftauto.com/
*
*****************************************************************************/

#include <StdInc.h>
#include <sstream>
#include <sys/stat.h>
#include <thread>
#include <chrono>

// Include internal header.
#include "fsinternal/CFileSystem.internal.h"

// Sub modules.
#include "fsinternal/CFileSystem.platform.h"
#include "fsinternal/CFileSystem.stream.raw.h"
#include "fsinternal/CFileSystem.translator.system.h"
#include "fsinternal/CFileSystem.platformutils.hxx"

#include "CFileSystem.Utils.hxx"

// Include native platform utilities.
#include "fsinternal/CFileSystem.internal.nativeimpl.hxx"

// Include threading utilities for CFileSystem class.
#include "CFileSystem.lock.hxx"

using namespace FileSystem;

CFileSystem *fileSystem = NULL;
CFileTranslator *fileRoot = NULL;

// Create the class at runtime initialization.
CSystemCapabilities systemCapabilities;

// Constructor of the CFileSystem instance.
// Every driver should register itself in this.
fileSystemFactory_t _fileSysFactory;

// Allocator of plugin meta-data.
struct _fileSystemAllocator
{
    inline void* Allocate( size_t memSize )
    {
        return new char[ memSize ];
    }

    inline void Free( void *memPtr, size_t memSize )
    {
        delete [] (char*)memPtr;
    }
};
static _fileSystemAllocator _memAlloc;

// Global string for the double-dot.
const filePath _dirBack( ".." );

/*=======================================
    CFileSystem

    Management class with root-access functions.
    These methods are root-access. Exposing them
    to a security-critical user-space context is
    not viable.
=======================================*/
static bool _hasBeenInitialized = false;

// Integrity check function.
// If this fails, then CFileSystem cannot boot.
inline bool _CheckLibraryIntegrity( void )
{
    // Check all data types.
    bool isValid = true;

    if ( sizeof(fsBool_t) != 1 ||
         sizeof(fsChar_t) != 1 || sizeof(fsUChar_t) != 1 ||
         sizeof(fsShort_t) != 2 || sizeof(fsUShort_t) != 2 ||
         sizeof(fsInt_t) != 4 || sizeof(fsUInt_t) != 4 ||
         sizeof(fsWideInt_t) != 8 || sizeof(fsUWideInt_t) != 8 ||
         sizeof(fsFloat_t) != 4 ||
         sizeof(fsDouble_t) != 8 )
    {
        isValid = false;
    }

#ifdef _DEBUG
    if ( !isValid )
    {
        // Notify the developer.
        __debugbreak();
    }
#endif //_DEBUG

    return isValid;
}

// Internal plugins.
fsLockProvider _fileSysLockProvider;

// Sub modules.
extern void registerRandomGeneratorExtension( const fs_construction_params& params );
extern void registerFileSystemMemoryMappedStreams( void );
extern void registerFileDataPresenceManagement( const fs_construction_params& params );

extern void unregisterFileSystemMemoryMappedStreams( void );
extern void unregisterRandomGeneratorExtension( void );
extern void unregisterFileDataPresenceManagement( void );

AINLINE void InitializeLibrary( const fs_construction_params& params )
{
    // Register addons.
    registerRandomGeneratorExtension( params );
    _fileSysLockProvider.RegisterPlugin( params );
    registerFileSystemMemoryMappedStreams();
    registerFileDataPresenceManagement( params );
    
    CFileSystemNative::RegisterZIPDriver( params );
    CFileSystemNative::RegisterIMGDriver( params );
}

AINLINE void ShutdownLibrary( void )
{
    // Unregister all addons.
    CFileSystemNative::UnregisterIMGDriver();
    CFileSystemNative::UnregisterZIPDriver();

    unregisterFileDataPresenceManagement();
    unregisterFileSystemMemoryMappedStreams();
    _fileSysLockProvider.UnregisterPlugin();
    unregisterRandomGeneratorExtension();
}

struct fs_constructor
{
    const fs_construction_params& params;

    inline fs_constructor( const fs_construction_params& params ) : params( params )
    {
        return;
    }

    inline CFileSystemNative* Construct( void *mem ) const
    {
        return new (mem) CFileSystemNative( this->params );
    }
};

// Creators of the CFileSystem instance.
// Those are the entry points to this (static) library.
CFileSystem* CFileSystem::Create( const fs_construction_params& params )
{
    // Make sure that there is no second CFileSystem class alive.
    assert( _hasBeenInitialized == false );

    // Make sure our environment can run CFileSystem in the first place.
    bool isLibraryBootable = _CheckLibraryIntegrity();

    if ( !isLibraryBootable )
    {
        // We failed some critical integrity tests.
        return NULL;
    }

    InitializeLibrary( params );

    CFileSystem *readyInstance = NULL;

    try
    {
        // Create our CFileSystem instance!
        fs_constructor constructor( params );

        CFileSystemNative *instance = _fileSysFactory.ConstructTemplate( _memAlloc, constructor );

        if ( instance )
        {
            // Get the application current directory and store it in "fileRoot" global.
            try
            {
                wchar_t cwd[1024];
                _wgetcwd( cwd, 1023 );

                // Make sure it is a correct directory
                filePath cwd_ex( cwd );
                cwd_ex += L'\\';

                // Every application should be able to access itself
                fileRoot = instance->CreateTranslator( cwd_ex.w_str() );
            }
            catch( ... )
            {
                _fileSysFactory.Destroy( _memAlloc, instance );

                throw;
            }

            // We have initialized ourselves.
            _hasBeenInitialized = true;
        }

        readyInstance = instance;
    }
    catch( ... )
    {
        // We do not want to pass on exceptions.
        _hasBeenInitialized = false;

        // Continue.
    }

    if ( !_hasBeenInitialized )
    {
        ShutdownLibrary();
    }

    return readyInstance;
}

void CFileSystem::Destroy( CFileSystem *lib )
{
    CFileSystemNative *nativeLib = (CFileSystemNative*)lib;

    assert( nativeLib != NULL );

    if ( nativeLib )
    {
        try
        {
            // Delete the main fileRoot access point.
            if ( CFileTranslator *rootAppDir = fileRoot )
            {
                delete rootAppDir;

                fileRoot = NULL;
            }

            _fileSysFactory.Destroy( _memAlloc, nativeLib );
        
            ShutdownLibrary();
        }
        catch( ... )
        {
            // Must not throw exceptions.
            abort();

            throw;
        }

        // We have successfully destroyed FileSystem activity.
        _hasBeenInitialized = false;
    }
}

#ifdef _WIN32

struct MySecurityAttributes
{
    DWORD count;
    LUID_AND_ATTRIBUTES attr[2];
};

#endif //_WIN32

CFileSystem::CFileSystem( const fs_construction_params& params )
{
    // Set up members.
    m_includeAllDirsInScan = false;
#ifdef _WIN32
    m_hasDirectoryAccessPriviledge = false;
#endif //_WIN32

    // Set the global fileSystem variable.
    fileSystem = this;

#ifdef _WIN32
    // Check for legacy paths.
    // We will have to convert certain paths if that is the case.
    bool isLegacyOS = false;
    {
        OSVERSIONINFOEXA winInfo;
        winInfo.dwOSVersionInfoSize = sizeof(winInfo);
        winInfo.dwMajorVersion = 6;
        winInfo.dwMinorVersion = 1;

        // Check if we are the on the same branch.
        DWORDLONG verCompMask = 0;

        VER_SET_CONDITION( verCompMask, VER_MAJORVERSION, VER_EQUAL );
        VER_SET_CONDITION( verCompMask, VER_MINORVERSION, VER_GREATER_EQUAL );

        BOOL doesSatisfy =
            VerifyVersionInfoA(
                &winInfo,
                VER_MAJORVERSION | VER_MINORVERSION,
                verCompMask
            );

        if ( doesSatisfy == FALSE )
        {
            // Check if we are much newer.
            verCompMask = 0;
            
            VER_SET_CONDITION( verCompMask, VER_MAJORVERSION, VER_GREATER_EQUAL );

            doesSatisfy =
                VerifyVersionInfoA(
                    &winInfo,
                    VER_MAJORVERSION,
                    verCompMask
                );
        }

        isLegacyOS = ( doesSatisfy == FALSE );
    }

    this->m_win32HasLegacyPaths = isLegacyOS;
#endif //_WIN32
}

CFileSystem::~CFileSystem( void )
{
    // Zero the main FileSystem access point.
    fileSystem = NULL;
}

bool CFileSystem::IsInLegacyMode( void ) const
{
#ifdef _WIN32
    if ( this->m_win32HasLegacyPaths )
        return true;
#endif //_WIN32

    return false;
}

bool CFileSystem::CanLockDirectories( void )
{
#ifdef FILESYS_MULTI_THREADING
    NativeExecutive::CReadWriteWriteContextSafe <> consistency( _fileSysLockProvider.GetReadWriteLock( this ) );
#endif //FILESYS_MULTI_THREADING

    // We should set special priviledges for the application if
    // running under Win32.
#ifdef _WIN32
    // We assume getting the priviledge once is enough.
    if ( !m_hasDirectoryAccessPriviledge )
    {
        HANDLE token;

        // We need SE_BACKUP_NAME to gain directory access on Windows
        if ( OpenProcessToken( GetCurrentProcess(), TOKEN_ADJUST_PRIVILEGES | TOKEN_QUERY, &token ) == TRUE )
        {
            MySecurityAttributes priv;

            priv.count = 2; // we want to request two priviledges.

            BOOL backupRequest = LookupPrivilegeValue( NULL, SE_BACKUP_NAME, &priv.attr[0].Luid );

            priv.attr[0].Attributes = SE_PRIVILEGE_ENABLED;

            BOOL restoreRequest = LookupPrivilegeValue( NULL, SE_RESTORE_NAME, &priv.attr[1].Luid );

            priv.attr[1].Attributes = SE_PRIVILEGE_ENABLED;

            if ( backupRequest == TRUE && restoreRequest == TRUE )
            {
                BOOL success = AdjustTokenPrivileges( token, false, (TOKEN_PRIVILEGES*)&priv, sizeof( priv ), NULL, NULL );

                if ( success == TRUE )
                {
                    m_hasDirectoryAccessPriviledge = true;
                }
            }

            CloseHandle( token );
        }
    }
    return m_hasDirectoryAccessPriviledge;
#elif defined(__linux__)
    // We assume that we can always lock directories under Unix.
    // This is actually a lie, because it does not exist.
    return true;
#else
    // No idea about directory locking on other environments.
    return false;
#endif //OS DEPENDENT CODE
}

template <typename charType>
static AINLINE bool _File_ParseSystemRootDescriptor(
    const charType *path,
    dirTree& tree, bool& bFile,
#ifdef _WIN32
    eRootPathType& pathTypeOut,
    filePath& uncPartOut,
    const charType*& uncEndOut,
#endif
    filePath& descOut
)
{
#ifdef _WIN32
    // We have to handle absolute path, too
    filePath uncPart;           // valid if pathType == eRootPathType::UNC
    const charType *uncEnd;     // helper variable for UNC paths.

    if ( _File_IsAbsolutePath( path ) )
    {
        if (!_File_ParseRelativePath( path + 3, tree, bFile ))
            return NULL;

        descOut += path[0];
        descOut += ":/";

        pathTypeOut = eRootPathType::DISK;

        return true;
    }
    else if ( _File_IsUNCPath( path, uncEnd, uncPart ) )
    {
        if (!_File_ParseRelativePath( uncEnd, tree, bFile ))
            return NULL;

        descOut += "//";
        descOut += uncPart;
        descOut += "/";

        pathTypeOut = eRootPathType::UNC;

        uncEndOut = uncEnd;
        uncPartOut = std::move( uncPart );

        return true;
    }
#elif defined(__linux__)
    if ( *path == '/' || *path == '\\' )
    {
        if (!_File_ParseRelativePath( path + 1, tree, bFile ))
            return NULL;

        descOut = "/";

        return true;
    }
#endif //OS DEPENDANT CODE

    return false;
}

template <typename charType>
bool CFileSystemNative::GenGetSystemRootDescriptor( const charType *path, filePath& descOut ) const
{
#ifdef _WIN32
    eRootPathType pathType;
    filePath uncPart;
    const charType *uncEnd;
#endif

    dirTree tree;
    bool file;

    return _File_ParseSystemRootDescriptor(
        path, tree, file,
#ifdef _WIN32
        pathType, uncPart, uncEnd,
#endif
        descOut
    );
}

bool CFileSystem::GetSystemRootDescriptor( const char *path, filePath& descOut ) const      { return ((CFileSystemNative*)this)->GenGetSystemRootDescriptor( path, descOut ); }
bool CFileSystem::GetSystemRootDescriptor( const wchar_t *path, filePath& descOut ) const   { return ((CFileSystemNative*)this)->GenGetSystemRootDescriptor( path, descOut ); }

template <typename charType>
CFileTranslator* CFileSystemNative::GenCreateTranslator( const charType *path, eDirOpenFlags flags )
{
    // Without access to directory locking, this function can not execute.
    if ( !CanLockDirectories() )
        return NULL;

    // THREAD-SAFE, because this function does not use shared-state variables.

    filePath root;
    dirTree tree;
    bool bFile;

#ifdef _WIN32
    eRootPathType pathType = eRootPathType::UNKNOWN;

    // We have to handle absolute path, too
    filePath uncPart;           // valid if pathType == ROOTPATH_UNC
    const charType *uncEnd;     // helper variable for UNC paths.
#endif

    bool isSystemPath =
        _File_ParseSystemRootDescriptor <charType> (
            path, tree, bFile,
#ifdef _WIN32
            pathType, uncPart, uncEnd,
#endif
            root
        );
    
    if ( !isSystemPath )
    {
        // Try a relative path from the current system directory.
        wchar_t pathBuffer[1024];
        _wgetcwd( pathBuffer, NUMELMS(pathBuffer) - 1 );

        pathBuffer[ NUMELMS( pathBuffer ) - 1 ] = 0;

        root += pathBuffer;
        root += "\\";
        root += path;

#ifdef _WIN32
        const wchar_t *parseUNCEnd; // helper variable.
        const wchar_t *parsePath = root.w_str();

        if ( _File_IsAbsolutePath( parsePath ) )
        {
            if (!_File_ParseRelativePath( parsePath + 3, tree, bFile ))
                return NULL;

            root.resize( 2 );
            root += "/";

            pathType = eRootPathType::DISK;
        }
        else if ( _File_IsUNCPath( parsePath, parseUNCEnd, uncPart ) )
        {
            if (!_File_ParseRelativePath( uncEnd, tree, bFile ))
                return NULL;

            root.resize( ( parseUNCEnd - parsePath ) + 1 );

            pathType = eRootPathType::UNC;
        }
        else
        {
            throw filesystem_exception( eGenExceptCode::INVALID_SYSPARAM );
        }
#elif defined(__linux__)
        if (!_File_ParseRelativePath( root.w_str() + 1, tree, bFile ))
            return NULL;

        root = "/";
#endif //OS DEPENDANT CODE
    }

    if ( bFile )
        tree.pop_back();

    _File_OutputPathTree( tree, false, root );

#ifdef _WIN32
    HANDLE dir = _FileWin32_OpenDirectoryHandle( root, flags );

    if ( dir == INVALID_HANDLE_VALUE )
        return NULL;
#elif defined(__linux__)
    DIR *dir = opendir( root.c_str() );

    if ( dir == NULL )
        return NULL;
#else
    if ( !IsDirectory( root.c_str() ) )
        return NULL;
#endif //OS DEPENDANT CODE

    try
    {
        CSystemFileTranslator *pTranslator = new CSystemFileTranslator();
        pTranslator->m_root = std::move( root );        // does not throw exceptions.
        pTranslator->m_rootTree = std::move( tree );    // does not throw exceptions.

#ifdef _WIN32
        pTranslator->m_pathType = pathType;
    
        if ( pathType == eRootPathType::UNC )
        {
            pTranslator->m_unc = std::move( uncPart );  // does not throw exceptions.
        }

        pTranslator->m_rootHandle = dir;
        pTranslator->m_curDirHandle = NULL;
#elif defined(__linux__)
        pTranslator->m_rootHandle = dir;
        pTranslator->m_curDirHandle = NULL;
#endif //OS DEPENDANT CODE

        return pTranslator;
    }
    catch( ... )
    {
        // Release the OS handle.
        CloseHandle( dir );

        throw;
    }
}

CFileTranslator* CFileSystem::CreateTranslator( const char *path, eDirOpenFlags flags )         { return ((CFileSystemNative*)this)->GenCreateTranslator( path, flags ); }
CFileTranslator* CFileSystem::CreateTranslator( const wchar_t *path, eDirOpenFlags flags )      { return ((CFileSystemNative*)this)->GenCreateTranslator( path, flags ); }

template <typename charType>
AINLINE CFileTranslator* CFileSystemNative::GenCreateSystemMinimumAccessPoint( const charType *path, eDirOpenFlags flags )
{
    filePath root;
    dirTree tree;
    bool bFile;

#ifdef _WIN32
    eRootPathType pathType = eRootPathType::UNKNOWN;

    // We have to handle absolute path, too
    filePath uncPart;           // valid if pathType == eRootPathType::UNC
    const charType *uncEnd;     // helper variable for UNC paths.
#endif

    bool isSystemPath =
        _File_ParseSystemRootDescriptor <charType> (
            path, tree, bFile,
#ifdef _WIN32
            pathType, uncPart, uncEnd,
#endif
            root
        );

    if ( !isSystemPath )
        return NULL;

    if ( bFile )
    {
        tree.pop_back();
    }

    // Try creating in the root itself.
    if ( CFileTranslator *rootTrans = CreateTranslator( root ) )
    {
        return rootTrans;
    }

    // Try creating the translator starting from the root.
    size_t n = 0;

    while ( true )
    {
        if ( n >= tree.size() )
            break;

        const filePath& curAdd = tree[ n++ ];

        root += curAdd;
        root += '/';

        CFileTranslator *tryTrans = CreateTranslator( root );

        if ( tryTrans )
        {
            return tryTrans;
        }
    }

    return NULL;
}

CFileTranslator* CFileSystem::CreateSystemMinimumAccessPoint( const char *path, eDirOpenFlags flags )       { return ((CFileSystemNative*)this)->GenCreateSystemMinimumAccessPoint( path, flags ); }
CFileTranslator* CFileSystem::CreateSystemMinimumAccessPoint( const wchar_t *path, eDirOpenFlags flags )    { return ((CFileSystemNative*)this)->GenCreateSystemMinimumAccessPoint( path, flags ); }

CArchiveTranslator* CFileSystem::GetArchiveTranslator( CFileTranslator *fileTrans )
{
    return dynamic_cast <CArchiveTranslator*> ( fileTrans );
}

CFile* CFileSystem::GenerateRandomFile( CFileTranslator *root, bool forcedReliability )
{
    // If forcedReliability == true, the runtime tells us that it cannot cope with failure
    // where success was probable to happen.

retryForReliability:
    // We try 42 times to create a randomly named file.
    unsigned long numAttempts = 0;

    while ( numAttempts++ < 42 )
    {
        // Generate some random filename.
        std::string fileName = "$rnd";
        fileName += std::to_string( fsrandom::getSystemRandom( this ) );

        CFile *genFile = root->Open( fileName.c_str(), "wb+" );

        if ( genFile )
        {
            return genFile;
        }
    }

    if ( forcedReliability )
    {
        using namespace std::chrono_literals;

        // We probably should wait a little.
        std::this_thread::sleep_for( 5ms );

        goto retryForReliability;
    }

    // We failed. This is a valid outcome.
    return NULL;
}

bool CFileSystem::IsDirectory( const char *path )
{
    return File_IsDirectoryAbsolute( path );
}

#ifdef _WIN32

// By definition, crash dumps are OS dependant.
// Currently we only support crash dumps on Windows OS.

bool CFileSystem::WriteMiniDump( const char *path, _EXCEPTION_POINTERS *except )
{
#if 0
    CRawFile *file = (CRawFile*)fileRoot->Open( path, "wb" );
    MINIDUMP_EXCEPTION_INFORMATION info;

    if ( !file )
        return false;

    // Create an exception information struct
    info.ThreadId = GetCurrentThreadId();
    info.ExceptionPointers = except;
    info.ClientPointers = false;

    // Write the dump
    MiniDumpWriteDump( GetCurrentProcess(), GetCurrentProcessId(), file->m_file, MiniDumpNormal, &info, NULL, NULL );

    delete file;
#endif

    return true;
}
#endif //_WIN32

bool CFileSystem::Exists( const char *path )
{
    struct stat tmp;

    return stat( path, &tmp ) == 0;
}

size_t CFileSystem::Size( const char *path )
{
    struct stat stats;

    if ( stat( path, &stats ) != 0 )
        return 0;

    return stats.st_size;
}

// Utility to quickly load data from files on the local filesystem.
// Do not export it into user-space since this function has no security restrictions.
bool CFileSystem::ReadToBuffer( const char *path, std::vector <char>& output )
{
#ifdef _WIN32
    HANDLE file = CreateFileA( path, GENERIC_READ, 0, NULL, OPEN_EXISTING, 0, NULL );

    if ( file == INVALID_HANDLE_VALUE )
        return false;

    size_t size = GetFileSize( file, NULL );

    if ( size != 0 )
    {
        output.resize( size );
        output.reserve( size );

        DWORD _pf;

        ReadFile( file, &output[0], (DWORD)size, &_pf, NULL );
    }
    else
        output.clear();

    CloseHandle( file );
    return true;
#elif defined(__linux__)
    int iReadFile = open( path, O_RDONLY, 0 );

    if ( iReadFile == -1 )
        return false;

    struct stat read_info;

    if ( fstat( iReadFile, &read_info ) != 0 )
        return false;

    if ( read_info.st_size != 0 )
    {
        output.resize( read_info.st_size );
        output.reserve( read_info.st_size );

        ssize_t numRead = read( iReadFile, &output[0], read_info.st_size );

        if ( numRead == 0 )
        {
            close( iReadFile );
            return false;
        }

        if ( numRead != read_info.st_size )
            output.resize( numRead );
    }
    else
        output.clear();

    close( iReadFile );
    return true;
#else
    return false;
#endif //OS DEPENDANT CODE
}
